前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • Vue

    • 从源码解读Vue生命周期
    • 组件的本质
    • 有状态组件的设计
    • 设计 VNode
    • 辅助创建 VNode 的 h 函数
    • 自定义渲染器和异步渲染
    • 渲染器之挂载
    • 渲染器的核心 Diff 算法
    • 渲染器之patch
    • 图解 Vue 响应式原理
    • 图解 Vue 异步更新
    • 剖析 Vue 内部运行机制
    • vue响应式原理模拟
    • vue状态管理之vuex
    • 理解Vue的设计思想及实现Vue
    • diff算法深入
    • vue router vuex原理分析
    • Vue3初探响应式原理.
    • vue2源码分析
    • vue组件化实践
  • React

  • Webpack

  • Node

  • Javascript

  • 综合

文章学习导航
面试总览

30 秒速记

  • h 把便于书写的输入整理成虚拟节点
  • 标签或组件决定节点种类
  • 文本、数组和插槽分别规范化
  • 不自动替业务推导稳定身份
  • 生成描述与真正挂载分开

h 像虚拟节点的构造助手,让开发者不必手写每个内部字段。 它接收元素或组件、属性以及子内容,再把文本、列表和插槽整理成渲染器需要的形式。比如 h('div', { class: 'box' }, '你好') 描述一个带文字的元素,但不会立刻修改页面。列表身份仍由业务提供稳定键,位置编号无法自动代表数据身份。

版本校准: 本文出现 Object.defineProperty、Dep、Watcher 和双端 Diff 时,主要描述 Vue 2 实现;Vue 3 使用 Proxy、effect 与新的渲染器路径。Vue 2 已于 2023-12-31 结束维护,新项目应以 Vue 3 为基线,旧项目参考 Vue 2 EOL 官方说明 制定迁移与安全策略。

自从有了 VNode ,开发页面的方式就变成了书写 VNode,但如果日常开发中需要手写 VNode ,那绝对是反人类的,在“组件的本质”一章中我们使用了 snabbdom 的 h 函数来辅助讲解一些小例子,h 函数作为创建 VNode 对象的函数封装,在一定程度上改善了这个问题,但却没有解决本质问题,这也是为什么我们需要模板或 jsx 的原因。但 h 函数依然很重要,因为无论是模板还是 jsx 都需要经过编译,那么是直接编译成 VNode 树好呢?还是编译成由 h 函数组成的调用集合好呢?这个其实很难说,但可以肯定的一点是,我们将可公用、灵活、复杂的逻辑封装成函数,并交给运行时,这能够大大降低编译器的书写难度,甚至经过编译器生成的代码也具有一定的可读性,而 h 函数就是众多运行时函数中的一个。

# 在VNode创建时确定其类型 - flags

一个最简单的 h 函数如下:

function h() {
  return {
    _isVNode: true,
    flags: VNodeFlags.ELEMENT_HTML,
    tag: 'h1',
    data: null,
    children: null,
    childFlags: ChildrenFlags.NO_CHILDREN,
    el: null
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

这个 h 函数只能用来创建一个空的 <h1></h1> 标签,可以说没有任何意义。为了让 h 函数更加灵活,我们可以增加一些参数,问题是哪些内容应该提取到参数中呢?如果提取的参数多了,就会导致函数的使用不便,如果提取的参数少了又会导致函数的功能不足,所以这也是一个探索的过程。实际上只需要把 tag、data 和 children 提取为参数即可:

function h(tag, data = null, children = null) {
  //...
}
@前端进阶之旅: 代码已经复制到剪贴板

我们来看看为什么三个参数就能满足需求,对于 _isVNode 属性,它的值始终都为 true,所以不需要提取到参数中。对于 flags 属性,我们可以通过检查 tag 属性值的特征来确定该 VNode 的 flags 属性值,如下:

function h(tag, data = null, children = null) {
  let flags = null
  if (typeof tag === 'string') {
    flags = tag === 'svg' ? VNodeFlags.ELEMENT_SVG : VNodeFlags.ELEMENT_HTML
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

如上代码所示,如果 tag 是字符串则可以确定该 VNode 是标签元素,再次通过 tag === 'svg' 进一步判断是否是 SVG 标签,从而确定了该 VNode 的类型。

TIP

请注意区分下文中出现的 tag,有时指的是 VNode 对象的 tag 属性,有时指的是 h 函数的第一个参数。

对于 Fragment 类型的 VNode,它的 tag 属性值为 null,但是纯文本类型的 VNode 其 tag 属性值也是 null,所以为了区分,我们可以增加一个唯一的标识,当 h 函数的第一个参数(tag)的值等于该标识的时候,则意味着创建的是 Fragment 类型的 VNode:

// 唯一标识
export const Fragment = Symbol()
function h(tag, data = null, children = null) {
  let flags = null
  if (typeof tag === 'string') {
    flags = tag === 'svg' ? VNodeFlags.ELEMENT_SVG : VNodeFlags.ELEMENT_HTML
  } else if (tag === Fragment) {
    flags = VNodeFlags.FRAGMENT
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

这时我们可以像如下这样调用 h 函数创建 Fragment:

import { h, Fragment } from 'vue'

h(Fragment, null, children)
@前端进阶之旅: 代码已经复制到剪贴板

类似的,对于Portal 类型的 VNode,它的 tag 属性值也可以是字符串,这就会与普通标签元素类型的 VNode 冲突,导致无法区分一个 VNode 到底是普通标签元素还是 Portal,所以我们参考 Fragment 的实现方式,增加一个 Portal 标识:

export const Fragment = Symbol()
export const Portal = Symbol()
function h(tag, data = null, children = null) {
  let flags = null
  if (typeof tag === 'string') {
    flags = tag === 'svg' ? VNodeFlags.ELEMENT_SVG : VNodeFlags.ELEMENT_HTML
  } else if (tag === Fragment) {
    flags = VNodeFlags.FRAGMENT
  } else if (tag === Portal) {
    flags = VNodeFlags.PORTAL
    tag = data && data.target
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

这里需要注意的一点是,类型为 Portal 的 VNode,其 tag 属性值存储的是 Portal 挂载的目标,即 target。通常模板在经过编译后,我们把 target 数据存储在 VNodeData 中,所以我们看到如上代码中包含 tag = data && data.target。

如果一个 VNode 对象的 tag 属性值不满足以上全部条件,那只有一种可能了,即该 VNode 是组件。有的同学可能会说,也有可能是文本节点啊,没错,但是我们很少会用 h 函数去创建一个文本节点,而是单纯的使用字符串,然后在内部实现中检测到该字符串的寓意是文本节点的时候会为其自动创建一个纯文本的 VNode 对象,例如:

h(
  'div',
  {
    style: { color: 'red' }
  },
  '我是文本'
)
@前端进阶之旅: 代码已经复制到剪贴板

如上代码所示,h 函数的第三个参数 children 的值是一段文本字符串,这时在 h 函数内部会为这段文本字符串创建一个与之相符的纯文本 VNode 对象。

我们回过头来,继续讨论当一个 VNode 是组件时,如何根据 tag 属性确定该 VNode 对象的 flags 值,很简单如下:

// 省略...
function h(tag, data = null, children = null) {
  let flags = null
  if (typeof tag === 'string') {
    flags = tag === 'svg' ? VNodeFlags.ELEMENT_SVG : VNodeFlags.ELEMENT_HTML
  } else if (tag === Fragment) {
    flags = VNodeFlags.FRAGMENT
  } else if (tag === Portal) {
    flags = VNodeFlags.PORTAL
    tag = data && data.target
  } else {
    // 兼容 Vue2 的对象式组件
    if (tag !== null && typeof tag === 'object') {
      flags = tag.functional
        ? VNodeFlags.COMPONENT_FUNCTIONAL       // 函数式组件
        : VNodeFlags.COMPONENT_STATEFUL_NORMAL  // 有状态组件
    } else if (typeof tag === 'function') {
      // Vue3 的类组件
      flags = tag.prototype && tag.prototype.render
        ? VNodeFlags.COMPONENT_STATEFUL_NORMAL  // 有状态组件
        : VNodeFlags.COMPONENT_FUNCTIONAL       // 函数式组件
    }
  }
}
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • Vue

    • 从源码解读Vue生命周期
    • 组件的本质
    • 有状态组件的设计
    • 设计 VNode
    • 辅助创建 VNode 的 h 函数
      • 在VNode创建时确定其类型 - flags
      • 在VNode创建时确定其children的类型
      • 使用 h 函数创建 VNode
    • 自定义渲染器和异步渲染
    • 渲染器之挂载
    • 渲染器的核心 Diff 算法
    • 渲染器之patch
    • 图解 Vue 响应式原理
    • 图解 Vue 异步更新
    • 剖析 Vue 内部运行机制
    • vue响应式原理模拟
    • vue状态管理之vuex
    • 理解Vue的设计思想及实现Vue
    • diff算法深入
    • vue router vuex原理分析
    • Vue3初探响应式原理.
    • vue2源码分析
    • vue组件化实践
  • React

  • Webpack

  • Node

  • Javascript

  • 综合

完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

Vue辅助创建 VNode 的 h 函数|原理篇

文章学习导航
面试总览

30 秒速记

  • h 把便于书写的输入整理成虚拟节点
  • 标签或组件决定节点种类
  • 文本、数组和插槽分别规范化
  • 不自动替业务推导稳定身份
  • 生成描述与真正挂载分开

h 像虚拟节点的构造助手,让开发者不必手写每个内部字段。 它接收元素或组件、属性以及子内容,再把文本、列表和插槽整理成渲染器需要的形式。比如 h('div', { class: 'box' }, '你好') 描述一个带文字的元素,但不会立刻修改页面。列表身份仍由业务提供稳定键,位置编号无法自动代表数据身份。

版本校准: 本文出现 Object.defineProperty、Dep、Watcher 和双端 Diff 时,主要描述 Vue 2 实现;Vue 3 使用 Proxy、effect 与新的渲染器路径。Vue 2 已于 2023-12-31 结束维护,新项目应以 Vue 3 为基线,旧项目参考 Vue 2 EOL 官方说明 制定迁移与安全策略。

自从有了 VNode ,开发页面的方式就变成了书写 VNode,但如果日常开发中需要手写 VNode ,那绝对是反人类的,在“组件的本质”一章中我们使用了 snabbdom 的 h 函数来辅助讲解一些小例子,h 函数作为创建 VNode 对象的函数封装,在一定程度上改善了这个问题,但却没有解决本质问题,这也是为什么我们需要模板或 jsx 的原因。但 h 函数依然很重要,因为无论是模板还是 jsx 都需要经过编译,那么是直接编译成 VNode 树好呢?还是编译成由 h 函数组成的调用集合好呢?这个其实很难说,但可以肯定的一点是,我们将可公用、灵活、复杂的逻辑封装成函数,并交给运行时,这能够大大降低编译器的书写难度,甚至经过编译器生成的代码也具有一定的可读性,而 h 函数就是众多运行时函数中的一个。

# 在VNode创建时确定其类型 - flags

一个最简单的 h 函数如下:

function h() {
  return {
    _isVNode: true,
    flags: VNodeFlags.ELEMENT_HTML,
    tag: 'h1',
    data: null,
    children: null,
    childFlags: ChildrenFlags.NO_CHILDREN,
    el: null
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

这个 h 函数只能用来创建一个空的 <h1></h1> 标签,可以说没有任何意义。为了让 h 函数更加灵活,我们可以增加一些参数,问题是哪些内容应该提取到参数中呢?如果提取的参数多了,就会导致函数的使用不便,如果提取的参数少了又会导致函数的功能不足,所以这也是一个探索的过程。实际上只需要把 tag、data 和 children 提取为参数即可:

function h(tag, data = null, children = null) {
  //...
}
@前端进阶之旅: 代码已经复制到剪贴板

我们来看看为什么三个参数就能满足需求,对于 _isVNode 属性,它的值始终都为 true,所以不需要提取到参数中。对于 flags 属性,我们可以通过检查 tag 属性值的特征来确定该 VNode 的 flags 属性值,如下:

function h(tag, data = null, children = null) {
  let flags = null
  if (typeof tag === 'string') {
    flags = tag === 'svg' ? VNodeFlags.ELEMENT_SVG : VNodeFlags.ELEMENT_HTML
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

如上代码所示,如果 tag 是字符串则可以确定该 VNode 是标签元素,再次通过 tag === 'svg' 进一步判断是否是 SVG 标签,从而确定了该 VNode 的类型。

TIP

请注意区分下文中出现的 tag,有时指的是 VNode 对象的 tag 属性,有时指的是 h 函数的第一个参数。

对于 Fragment 类型的 VNode,它的 tag 属性值为 null,但是纯文本类型的 VNode 其 tag 属性值也是 null,所以为了区分,我们可以增加一个唯一的标识,当 h 函数的第一个参数(tag)的值等于该标识的时候,则意味着创建的是 Fragment 类型的 VNode:

// 唯一标识
export const Fragment = Symbol()
function h(tag, data = null, children = null) {
  let flags = null
  if (typeof tag === 'string') {
    flags = tag === 'svg' ? VNodeFlags.ELEMENT_SVG : VNodeFlags.ELEMENT_HTML
  } else if (tag === Fragment) {
    flags = VNodeFlags.FRAGMENT
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

这时我们可以像如下这样调用 h 函数创建 Fragment:

import { h, Fragment } from 'vue'

h(Fragment, null, children)
@前端进阶之旅: 代码已经复制到剪贴板

类似的,对于Portal 类型的 VNode,它的 tag 属性值也可以是字符串,这就会与普通标签元素类型的 VNode 冲突,导致无法区分一个 VNode 到底是普通标签元素还是 Portal,所以我们参考 Fragment 的实现方式,增加一个 Portal 标识:

export const Fragment = Symbol()
export const Portal = Symbol()
function h(tag, data = null, children = null) {
  let flags = null
  if (typeof tag === 'string') {
    flags = tag === 'svg' ? VNodeFlags.ELEMENT_SVG : VNodeFlags.ELEMENT_HTML
  } else if (tag === Fragment) {
    flags = VNodeFlags.FRAGMENT
  } else if (tag === Portal) {
    flags = VNodeFlags.PORTAL
    tag = data && data.target
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

这里需要注意的一点是,类型为 Portal 的 VNode,其 tag 属性值存储的是 Portal 挂载的目标,即 target。通常模板在经过编译后,我们把 target 数据存储在 VNodeData 中,所以我们看到如上代码中包含 tag = data && data.target。

如果一个 VNode 对象的 tag 属性值不满足以上全部条件,那只有一种可能了,即该 VNode 是组件。有的同学可能会说,也有可能是文本节点啊,没错,但是我们很少会用 h 函数去创建一个文本节点,而是单纯的使用字符串,然后在内部实现中检测到该字符串的寓意是文本节点的时候会为其自动创建一个纯文本的 VNode 对象,例如:

h(
  'div',
  {
    style: { color: 'red' }
  },
  '我是文本'
)
@前端进阶之旅: 代码已经复制到剪贴板

如上代码所示,h 函数的第三个参数 children 的值是一段文本字符串,这时在 h 函数内部会为这段文本字符串创建一个与之相符的纯文本 VNode 对象。

我们回过头来,继续讨论当一个 VNode 是组件时,如何根据 tag 属性确定该 VNode 对象的 flags 值,很简单如下:

// 省略...
function h(tag, data = null, children = null) {
  let flags = null
  if (typeof tag === 'string') {
    flags = tag === 'svg' ? VNodeFlags.ELEMENT_SVG : VNodeFlags.ELEMENT_HTML
  } else if (tag === Fragment) {
    flags = VNodeFlags.FRAGMENT
  } else if (tag === Portal) {
    flags = VNodeFlags.PORTAL
    tag = data && data.target
  } else {
    // 兼容 Vue2 的对象式组件
    if (tag !== null && typeof tag === 'object') {
      flags = tag.functional
        ? VNodeFlags.COMPONENT_FUNCTIONAL       // 函数式组件
        : VNodeFlags.COMPONENT_STATEFUL_NORMAL  // 有状态组件
    } else if (typeof tag === 'function') {
      // Vue3 的类组件
      flags = tag.prototype && tag.prototype.render
        ? VNodeFlags.COMPONENT_STATEFUL_NORMAL  // 有状态组件
        : VNodeFlags.COMPONENT_FUNCTIONAL       // 函数式组件
    }
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

在 Vue2 中用一个对象作为组件的描述,而在 Vue3 中,有状态组件是一个继承了基类的类。所以如果是 Vue2 的对象式组件,我们通过检查该对象的 functional 属性的真假来判断该组件是否是函数式组件。在 Vue3 中,因为有状态组件会继承基类,所以通过原型链判断其原型中是否有 render 函数的定义来确定该组件是否是有状态组件。

一旦确定了一个 VNode 的类型,那么 h 函数就可返回带有正确类型的 VNode:

function h(tag, data = null, children = null) {
  let flags = null
  // 省略用来确定 flags 的代码

  return {
    flags,
    // 其他属性...
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

# 在VNode创建时确定其children的类型

上文通过 检测 tag 属性值 来确定一个 VNode 对象的 flags 属性值。类似地,可以通过 检测 children 来确定 childFlags 的值。根据 h 函数的调用方式可以很容易地确定参数 children 包含哪些值:

  • 1、children 是一个数组:
h('ul', null, [
  h('li'),
  h('li')
])
@前端进阶之旅: 代码已经复制到剪贴板
  • 2、children 是一个 VNode 对象:
h('div', null, h('span'))
@前端进阶之旅: 代码已经复制到剪贴板
  • 3、没有 children:
h('div')
@前端进阶之旅: 代码已经复制到剪贴板
  • 4、children 是一个普通文本字符串:
h('div', null, '我是文本')
@前端进阶之旅: 代码已经复制到剪贴板

以上是四种常见的 h 函数的调用方式,根据这四种调用方式中 children 的不同形式即可确定一个 VNode 对象的 childFlags:

← 设计 VNode自定义渲染器和异步渲染 →

fe